#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <pthread.h>
#include <string.h>
#include <strings.h>

#include "radix.h"
#include "libxsp.h"
#include "psd_logger.h"
#include "psd_tpool.h"
#include "psd_config.h"

#include "psd_route.h"
#include "psd_radix.h"
#include "psd_traceroute.h"

radix_tree_t *routes = NULL;
pthread_mutex_t routes_lock;

static void psd_free_str(void *v);
static int __psd_route_list(radix_tree_t *route_tree);
static void *psd_route_refresh_thread(void *arg);

struct psd_route_config_t {
	char *route_file;
	int refresh_interval;
	int use_traceroute;
	char *traceroute_file;
	int traceroute_max_ttl;
	int traceroute_max_timeout;
};

struct psd_route_config_t psdRouteConfig = {
	.route_file = NULL,
	.refresh_interval = 0,
	.use_traceroute = 0,
	.traceroute_file = NULL,
	.traceroute_max_ttl = 0,
	.traceroute_max_timeout = 0
};

void psd_route_read_config() {
	char *str_val;
	int val;

	if (psd_depot_settings_get("route", "route_file", &str_val) == 0) {
		psdRouteConfig.route_file = str_val;
	}

	if (psd_depot_settings_get_int("route", "refresh_interval", &val) == 0) {
		psdRouteConfig.refresh_interval = val;
	}

	if (psd_depot_settings_get_bool("route", "use_traceroute", &val) == 0) {
		psdRouteConfig.use_traceroute = val;
	}

	if (psd_depot_settings_get("route", "traceroute_file", &str_val) == 0) {
		psdRouteConfig.traceroute_file = str_val;
	}

	if (psd_depot_settings_get_int("route", "traceroute_max_ttl", &val) == 0) {
		psdRouteConfig.traceroute_max_ttl = val;
	}

	if (psd_depot_settings_get_int("route", "traceroute_max_timeout", &val) == 0) {
		psdRouteConfig.traceroute_max_timeout = val;
	}
}

int psd_route_init() {

	psd_route_read_config();

	if(pthread_mutex_init(&routes_lock, 0) != 0) { 
		psd_err(0, "pthread_mutex_init failed");
		goto error_out;
	}

	routes = New_Radix(32);
	if (!routes) {
		psd_err(0, "New_Radix failed");
		goto error_exit_lock;
	}

	if (psdRouteConfig.use_traceroute) {
		if (psdRouteConfig.traceroute_file) {
			if (psd_traceroute_init(psdRouteConfig.traceroute_file, NULL, psdRouteConfig.traceroute_max_ttl, psdRouteConfig.traceroute_max_timeout) != 0) {
				psd_err(0, "failed to initialize traceroute system. disabling traceroute lookup");
				psdRouteConfig.use_traceroute = 0;
			}
		}
	}

	if (psdRouteConfig.route_file) {
		if (!psdRouteConfig.refresh_interval) {
			psd_route_read(psdRouteConfig.route_file);
		} else {
			if (psd_tpool_exec(psd_route_refresh_thread, NULL) != 0) {
				fprintf(stderr, "psd_route_refresh_init(): couldn't startup route refresh thread\n");
				goto error_exit_lock;
			}
		}
	}

	return 0;

error_exit_lock:
	pthread_mutex_destroy(&routes_lock);
error_out:
	return -1;
}

static void *psd_route_refresh_thread(void *arg) {
	while(1) {
		psd_route_read(psdRouteConfig.route_file);

		sleep(60*psdRouteConfig.refresh_interval);
	}
}

int psd_route_add(const char *addr_mask, const char *hop_id) {
	int retval;

	pthread_mutex_lock(&routes_lock);
	{
		retval = psd_radix_add(routes, addr_mask, strdup(hop_id));
	}
	pthread_mutex_unlock(&routes_lock);

	return retval;
}

int psd_route_remove(const char *addr_mask) {
	char *hop_id;

	pthread_mutex_lock(&routes_lock);
	{
		hop_id = psd_radix_remove(routes, addr_mask);
	}
	pthread_mutex_unlock(&routes_lock);

	free(hop_id);

	return 0;
}

int psd_route_read(const char *filename) {
	FILE *f;
	char *addrmask;
	char *next_hop;
	char line[1024];
	int linelen;
	int i;
	char *tmp;

	radix_tree_t *new_routes, *old_routes;

	/* initialize new route table */

	if (!filename)
		return -1;

	new_routes = New_Radix(32);

	/* read in routes from specified file */
	f = fopen(filename, "r");
	if (f == NULL) {
		psd_err(0, "file open failed: %s", strerror(errno));
		return -1;
	}

	bzero(line, 1024);

	while(fgets(line, 1024, f) != NULL) {
		linelen = strlen(line);
	
		for(i = 0; i < linelen; i++) {
			if (line[i] == '\n')
				line[i] = '\0';
		}

		tmp = strtok(line, "\t");
		if (!tmp) {
			psd_err(0, "not a valid route: %s", line);
			continue;
		}


		addrmask = strdup(tmp);
		if (!addrmask) {
			psd_err(0, "couldn't duplicate address/mask");
			continue;
		}

		tmp = strtok(NULL, "\t");
		if (!tmp) {
			psd_err(0, "not a valid route: %s", line);
			free(addrmask);
			continue;
		}

		next_hop = strdup(tmp);
		if (!next_hop) {
			psd_err(0, "couldn't duplicate next hop");
			free(addrmask);
			continue;
		}

		psd_radix_add(new_routes, addrmask, strdup(next_hop));

		free(addrmask);
		free(next_hop);
	}

	fclose(f);

	__psd_route_list(new_routes);

	pthread_mutex_lock(&routes_lock);
	old_routes = routes;
	routes = new_routes;
	pthread_mutex_unlock(&routes_lock);

	if(old_routes != NULL) {
		Destroy_Radix(old_routes, psd_free_str);
	}

	return(1);
}

static void psd_free_str(void *v) {
	char *str = (char *) v;

	free(str);
}

const char *psd_route_lookup(const char *hop_id) {
	const char *ret_id;

	psd_info(0, "looking up \"%s\"", hop_id);

	pthread_mutex_lock(&routes_lock);
	{
		ret_id = psd_radix_lookup(routes, hop_id);
	}
	pthread_mutex_unlock(&routes_lock);

	if (ret_id == NULL && psdRouteConfig.use_traceroute) {
		psd_info(0, "looking up via traceroute: \"%s\"", hop_id);
		ret_id = psd_traceroute_lookup(hop_id);
		// we could add the route so we don't have to traceroute later...
		if (ret_id) {
			struct addrinfo *hop_addr;
			hop_addr = xsp_lookuphop(hop_id);
			if (hop_addr) {
				struct addrinfo *curr_addr;
				for(curr_addr = hop_addr; curr_addr != NULL; curr_addr = curr_addr->ai_next) {
					char mask[255];
					prefix_t prefix;

                    bzero(&prefix, sizeof(prefix));

					prefix.bitlen = 32;
					prefix.family = curr_addr->ai_family;
					if (curr_addr->ai_family == AF_INET)
						prefix.add.sin = ((struct sockaddr_in *)curr_addr->ai_addr)->sin_addr;
#ifdef HAVE_IPV6
					else if (curr_addr->ai_family == AF_INET6)
						prefix.add.sin6 = ((struct sockaddr_in6 *)curr_addr->ai_addr)->sin6_addr;
#endif
					else
						continue;

					prefix_toa2(&prefix, mask);
					strlcat(mask, "/32", 255);
					psd_info(0, "Adding %s -> %s to routing table", mask, ret_id);
					psd_route_add(mask, ret_id);
				}

				freeaddrinfo(hop_addr);
			}
		}
	}

	return ret_id;
}

static int __psd_route_list(radix_tree_t *route_tree) {
	radix_node_t *node;
	char *hop_id;

	RADIX_WALK (route_tree->head, node) {
		hop_id = (char *) node->data;
		psd_info(8, "%s/%d -> %s",
				prefix_toa(node->prefix),
				node->prefix->bitlen,
				hop_id);
	} RADIX_WALK_END;
	return(0);
}
